#
# Bandleader
# Licensed under the GPL
#

import wx
import os

loaded_pygame = True

try:
    import pygame.mixer
except ImportError:
    loaded_pygame = False

class ListSlicer:
    """ Slices a list piece by piece. """

    def __init__(self, list, index=0):
        """ Provide the list to slice. The starting index defaults to 0."""
        self.list = list
        self.index = index 

    def slice(self, size):
        """ Returns a slice of the list. The slice has size elements. """
        index = min(self.index, len(self.list))
        max_index = min(index + size, len(self.list))
        self.index = max_index

        return self.list[index:max_index]

    def empty(self):
        """ Returns true if there are no more items to be sliced. """
        return self.index >= len(self.list)


class Cursor:
    def __init__(self):
        self.row = 0
        self.col = 0

    def move_up(self):
        if self.row > 0:
            self.row -= 1

    def move_down(self, num_rows):
        if self.row < num_rows-1:
            self.row += 1

    def move_left(self, num_cols):
        if self.col > 0:
            self.col -= 1
        else:
            if self.row > 0:
                # wrap around
                self.col = num_cols-1
                self.row -= 1

    def move_right(self, num_cols, num_rows):
        if self.col < num_cols-1:
            self.col += 1
        else:
            if self.row < num_rows-1:
                # wrap around
                self.col = 0
                self.row += 1

    def set_pos(self, row, col, num_rows, num_cols):
        if row >= 0 and row < num_rows and \
           col >= 0 and col < num_cols:
           self.row = row
           self.col = col

class ChordView(wx.Control):
    """ Displays chords in a grid. """

    longest_chord_allowed = "F#M13#11 "
    len_longest_chord_allowed = len(longest_chord_allowed) - 1

    def __init__(self, parent, status_bar=None):
        wx.Control.__init__(self, parent)

        for event, handler in \
            [(wx.EVT_KEY_DOWN, self.on_key_down),
             (wx.EVT_KEY_UP, self.on_key_up),
             (wx.EVT_CHAR, self.on_key_char),
             (wx.EVT_LEFT_DOWN, self.on_mouse_left_down),
             (wx.EVT_LEFT_UP, self.on_mouse_left_up),
             (wx.EVT_MOTION, self.on_mouse_motion),
             (wx.EVT_PAINT, self.on_paint),
             (wx.EVT_SIZE, self.on_resize)]:
            self.Bind(event, handler)

        # really beats per measure
        self.divisions_per_cell = 4
        self.cells_per_row = 2
        self.divisions_per_row = self.divisions_per_cell * self.cells_per_row

        self.chords = ['C', '', 'F', '', 'G', '', 'F', '',
                       'C', '', 'F', '', 'G', '', 'F', '',
                       'C', '', 'F', '', 'G', '', 'F', '',
                       'C', '', 'F', '', 'G', '', 'F', '']
        self.selection = set([(0, 5), (0, 6), (0, 7), (1, 0)])
        # columns are in division coords, rows are in cell coords
        self.cursor = Cursor()

        #
        self.input_text = ""
        self.input_cell = 0
        self.is_appending_text = False

        #
        self.document_name = "Untitled"
        self.file_path = ''

        #
        self.status_bar = status_bar

        #
        if loaded_pygame:
            pygame.mixer.init()

    def __del__(self):
        if loaded_pygame:
            pygame.mixer.quit()

    def repaint(self):
        self.Refresh(False)

    def get_num_rows(self):
        """ Returns the number of rows in the current document """
        return len(self.chords) / self.divisions_per_row

    def get_virtual_width(self):
        """ Returns the width the canvas actually takes up """
        return self.division_width * self.divisions_per_row

    def get_virtual_height(self):
        """ Returns the height the canvas actually takes up """
        return self.cell_height * self.get_num_rows()

    def move_cursor_up(self):
        self.cursor.move_up()
        self.commit_input()
        self.repaint()

    def move_cursor_down(self):
        self.cursor.move_down(self.get_num_rows())
        self.commit_input()
        self.repaint()

    def move_cursor_left(self):
        self.cursor.move_left(self.divisions_per_row)
        self.commit_input()
        self.repaint()

    def move_cursor_right(self):
        self.cursor.move_right(self.divisions_per_row, self.get_num_rows())
        self.commit_input()
        self.repaint()

    def move_cursor_word_left(self):
        for i in range(self.divisions_per_cell):
            self.cursor.move_left(self.divisions_per_row)
        self.commit_input()
        self.repaint()

    def move_cursor_word_right(self):
        num_rows = self.get_num_rows()

        for i in range(self.divisions_per_cell):
            self.cursor.move_right(self.divisions_per_row, num_rows)
        self.commit_input()
        self.repaint()

    def get_cursor_cell(self):
        return self.cursor.row * self.divisions_per_row + self.cursor.col

    def append_input(self, text):
        if len(self.input_text) >= ChordView.len_longest_chord_allowed:
            return

        if not self.is_appending_text:
            self.is_appending_text = True
            self.input_cell = self.get_cursor_cell()

        self.input_text += text
        self.repaint()

    def backspace_input(self):
        if self.is_appending_text:
            if len(self.input_text) > 0:
                last3chars = self.input_text[-3:]

                if last3chars == 'aug' or \
                   last3chars == 'maj' or \
                   last3chars == 'dim' or \
                   last3chars == 'sus':
                    self.input_text = self.input_text[:-3]
                else:
                    self.input_text = self.input_text[:-1]

                self.repaint()

    def end_input(self):
        if self.is_appending_text:
            # we're done inputting
            self.is_appending_text = False
            self.input_text = ""

            # put the text in the right place
            self.repaint()

    def commit_input(self):
        if self.is_appending_text and len(self.input_text) > 0:
            cell_index = self.input_cell

            # save the input
            if cell_index >= 0 and cell_index < len(self.chords):
                self.chords[cell_index] = self.input_text

        self.end_input()

    def delete_input_character(self):
        pass

    def delete_chord_under_cursor(self):
        cell_index = self.get_cursor_cell()

        if cell_index >= 0 and cell_index < len(self.chords):
            self.chords[cell_index] = ''
            self.repaint()

    def delete_key(self):
        if self.is_appending_text:
            self.delete_input_character()
        else:
            self.delete_chord_under_cursor()

    def toggle_play(self):
        if not loaded_pygame:
            return

        if self.is_appending_text:
            return

        if pygame.mixer.music.get_busy():
            self.stop()
        else:
            self.play()

    def on_key_down(self, e):
        if e.AltDown() or \
           e.MetaDown():
           e.Skip()
           return

        key_code = e.GetKeyCode()
        shift_down = e.ShiftDown()
        control_down = e.ControlDown()
        
        # caches
        input_size = len(self.input_text)
        if input_size > 0:
            last_char = self.input_text[-1]

        if control_down:
            if key_code == wx.WXK_LEFT:
                self.move_cursor_word_left()
            elif key_code == wx.WXK_RIGHT:
                self.move_cursor_word_right()
            else:
                e.Skip()
            #
            return

        if key_code == wx.WXK_UP:
            self.move_cursor_up()
        elif key_code == wx.WXK_DOWN:
            self.move_cursor_down()
        elif key_code == wx.WXK_LEFT:
            self.move_cursor_left()
        elif key_code == wx.WXK_RIGHT:
            self.move_cursor_right()
        elif key_code == wx.WXK_RETURN:
            self.move_cursor_right()
        elif key_code == wx.WXK_BACK:
            self.backspace_input()
        elif key_code == wx.WXK_DELETE:
            self.delete_key()
        elif key_code == wx.WXK_ESCAPE:
            self.end_input()
        elif key_code == wx.WXK_SPACE:
            self.toggle_play()
        elif key_code == 47: # /
            if input_size > 0:
                self.append_input('/')
        elif key_code == 49: # 1
            if input_size > 0:
                self.append_input('1')
        elif key_code == 50: # 2
            if input_size > 0:
                self.append_input('2')
        elif key_code == 51: # 3
            if input_size > 0:
                if last_char == '1':
                    self.append_input('3')
                else:
                    self.append_input('#')
        elif key_code == 52: # 4
            if input_size > 0:
                self.append_input('2')
        elif key_code == 53: # 5
            if input_size > 0:
                self.append_input('5')
        elif key_code == 54: # 6
            if input_size > 0:
                self.append_input('6')
        elif key_code == 55: # 7
            if input_size > 0:
                self.append_input('7')
        elif key_code == 57: # 9
            if input_size > 0:
                self.append_input('9')
        elif key_code == 65: # a
            if input_size == 0 or last_char == '/':
                self.append_input('A')
            else:
                self.append_input('aug')
        elif key_code == 66: # b
            if input_size == 0 or last_char == '/':
                self.append_input('B')
            else:
                self.append_input('b')
        elif key_code == 67: # c
            self.append_input('C')
        elif key_code == 68: # d
            if input_size == 0 or last_char == '/':
                self.append_input('D')
            else:
                self.append_input('dim')
        elif key_code == 69: # e
            self.append_input('E')
        elif key_code == 70: # f
            self.append_input('F')
        elif key_code == 71: # g
            self.append_input('G')
        elif key_code == 77: # m
            if input_size > 0:
                if shift_down:
                    self.append_input('maj')
                elif last_char == 'm':
                    self.backspace_input()
                    self.append_input('maj')
                else:
                    self.append_input('m')
        elif key_code == 83: # s
            if input_size > 0:
                self.append_input('sus')
        else:
            e.Skip()

    def on_key_up(self, e):
        e.Skip()

    def on_key_char(self, e):
        key_char = e.GetKeyCode()

        if key_char == 104: # h
            self.move_cursor_left()
        elif key_char == 106: # j
            self.move_cursor_down()
        elif key_char == 107: # k
            self.move_cursor_up()
        elif key_char == 108: # l
            self.move_cursor_right()
        elif key_char == 118: # v
            self.move_cursor_word_left()
        elif key_char == 119: # w
            self.move_cursor_word_right()
        elif key_char == 120: # x
            self.delete_key()
        else:
            e.Skip()

    def on_mouse_left_down(self, e):
        if self.is_appending_text:
            self.commit_input()

        (x, y) = e.GetPosition()

        num_rows = self.get_num_rows()

        col = x / self.division_width
        row = y / self.cell_height

        self.cursor.set_pos(row, col, num_rows, self.divisions_per_row)

        self.repaint()

    def on_mouse_left_up(self, e):
        pass

    def on_mouse_motion(self, e):
        pass

    def on_resize(self, e):
        pass

    def on_paint(self, e):
        dc = wx.BufferedPaintDC(self)
        dc.Clear()

        self.compute_dimensions(dc)
        self.draw_selection(dc)
        self.draw_cursor(dc)
        self.draw_command_line(dc)

        slicer = ListSlicer(self.chords)

        row = 0

        while not slicer.empty():
            row_chords = slicer.slice(self.divisions_per_row)
            self.draw_row(dc, row, row_chords)
            row += 1

    def compute_dimensions(self, dc):
        self.division_width, self.division_height = \
            dc.GetTextExtent(ChordView.longest_chord_allowed)

        self.cell_width = self.division_width * self.divisions_per_cell
        self.cell_height = self.division_height * 4

    def draw_row(self, dc, row, chords):
        y = row * self.cell_height

        slicer = ListSlicer(chords)

        for i in range(self.cells_per_row):
            cell_chords = slicer.slice(self.divisions_per_cell)

            if len(cell_chords) > 0:
                x = i * self.cell_width
                self.draw_cell(dc, cell_chords, (x, y))

    def draw_cell(self, dc, chords, position):
        (x, y) = position

        dc.SetBrush(wx.TRANSPARENT_BRUSH)
        dc.SetPen(wx.BLACK_PEN)
        dc.DrawRectangle(x, y, self.cell_width, self.cell_height)

        text_x = x

        for chord in chords:
            chord_text = ' ' + chord
            dc.DrawText(chord_text, text_x, y)
            text_x += self.division_width

    def color_cell(self, dc, row, col, brush):
        x = col * self.division_width
        y = row * self.cell_height

        dc.SetBrush(brush)

        dc.DrawRectangle(x, y, self.division_width, self.cell_height)

    def draw_cursor(self, dc):
        dc.SetPen(wx.TRANSPARENT_PEN)

        cursor = self.cursor
        self.color_cell(dc, cursor.row, cursor.col, wx.GREY_BRUSH)

    def draw_selection(self, dc):
        dc.SetPen(wx.TRANSPARENT_PEN)

        for cell in self.selection:
            (row, col) = cell
            self.color_cell(dc, row, col, wx.BLUE_BRUSH)

    def draw_command_line(self, dc):
        dc.SetPen(wx.BLACK_PEN)

        x = 0
        y = self.get_virtual_height() + 30

        dc.DrawText(self.input_text, x, y)

        # cursor
        font_width, font_height = \
            dc.GetTextExtent(self.input_text)
        dc.DrawLine(font_width, y, font_width, y + font_height)

    def is_untitled(self):
        return len(self.file_path) == 0

    def open(self, file_path):
        if len(file_path) > 0:
            self.file_path = file_path
            self.document_name = file_path

            f = open(file_path, 'r')
            self.open_as_bandleader(f)
            f.close()

    def open_as_bandleader(self, f):
        # TODO
        pass

    def save_as(self, file_path):
        self.file_path = file_path
        self.document_name = file_path # XXX
        self.save()

    def save(self):
        if len(self.file_path) > 0:
            f = open(self.file_path, 'w')
            self.save_into(f)
            f.close()

    def save_into(self, f):
        f.write(self.as_mma())

    def as_mma(self):
        text = ''
        beat = 1
        bar_number = 1

        text += '// Autogenerated by Bandleader\n\n'

        groove = 'Swing'
        text += 'Groove ' + groove + '\n\n'

        do_metronome = False
        if do_metronome:
            text += 'z * 2\n\n'

        for i in range(0, len(self.chords)):
            chord = self.chords[i]

            if beat == 1:
                if len(chord) == 0:
                    chord = '/'

                text += str(bar_number)
                text += ' '
                text += chord
                beat += 1
            elif beat == self.divisions_per_cell:
                if len(chord) == 0:
                    chord = '/'

                beat = 1
                bar_number += 1
                text += ' '
                text += chord
                text += '\n'
            else:
                if len(chord) == 0:
                    chord = '/'

                text += ' '
                text += chord
                beat += 1

        return text

    def temp_save(self):
        real_file_path = self.file_path

        file_path = os.path.join(os.getcwd(), "~tmp.mma")
        self.file_path = file_path
        self.save()

        self.file_path = real_file_path

        return file_path

    def generate_midi_file(self):
        self.set_status('Generating...')

        self.SetCursor(wx.HOURGLASS_CURSOR)

        file_path = self.temp_save()
        mma_path = r'c:\cygwin\home\Wallace\bin\mma.bat'
        os.system(mma_path + ' ' + file_path)

        # prepare it in the mixer
        midi_file = os.path.join(os.getcwd(), '~tmp.mid')
        pygame.mixer.music.load(midi_file)

        self.SetCursor(wx.STANDARD_CURSOR)

    def play(self):
        if not loaded_pygame:
            return

        self.generate_midi_file()

        pygame.mixer.music.play()

        self.set_status('Playing...')

    def stop(self):
        if not loaded_pygame:
            return

        if pygame.mixer.music.get_busy():
            pygame.mixer.music.stop()

            self.set_status('Stopped.')

    def set_status(self, text):
        if self.status_bar is not None:
            self.status_bar.SetStatusText(text)

